﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Manoso.Data.FluentMap
{
    internal interface IMapQuery : IMapSql
    {
        object SetValue(object parent, Action<object, IEnumerable<IMapProperty>> read);
        Dictionary<string, Action<object, object>> LinkActions { get; }
        Dictionary<string, Action<object, object>> SelfActions { get; }
    }

    public interface IMapQuery<T> : IMapSql<T>
    {
        /// <summary>
        /// Define a Primary Key column for a property.
        /// </summary>
        /// <typeparam name="TP">Property type.</typeparam>
        /// <param name="column">Column name.</param>
        /// <param name="lambda">Property lambda expression from T.</param>
        /// <param name="prefixAlias">Specify whether column alias contains table alias.</param>
        void Key<TP>(string column, Expression<Func<T, TP>> lambda, bool prefixAlias = false);

        /// <summary>
        /// Define a table column for a property.
        /// </summary>
        /// <typeparam name="TP">Property type.</typeparam>
        /// <param name="column">Column name.</param>
        /// <param name="lambda">Action to set the property.</param>
        /// <param name="prefixAlias">Specify whether column alias contains table alias.</param>
        void Column<TP>(string column, Action<T, TP> lambda, bool prefixAlias = false);

        /// <summary>
        /// Set self-reference recursive member for objects of T type at current map node. For it to work as expected, query result must be returned in their hierarchy order.
        /// </summary>
        /// <param name="match">Func to test whether two member match.</param>
        /// <param name="set">Action to set the self-reference property.</param>
        void Self(Func<T, T, bool> match, Action<T, T> set);

        /// <summary>
        /// Link to a reference object at higher level of hierarchy map for objects of current map node. 
        /// </summary>
        /// <typeparam name="TLink">The referred type defined at higher level.</typeparam>
        /// <param name="lambda">Action to set the property.</param>
        /// <param name="alias">Alias of the referred type.</param>
        void Link<TLink>(Action<T, TLink> lambda, string alias);
    }

    internal class MapQuery<T, TSub> : MapSql<TSub>, IMapQuery<TSub>, IMapQuery
    {
        public Dictionary<string, Action<object, object>> LinkActions { get; private set; }
        public Dictionary<string, Action<object, object>> SelfActions { get; private set; }

        public void Key<TP>(string column, Expression<Func<TSub, TP>> lambda, bool prefixAlias = false)
        {
            var member = lambda.GetMemberInfo<MemberInfo>();
            var mp = new MapProperty
            {
                Column = new MapColumn(column, prefixAlias ? Alias : null),
                HostAlias = TableAlias,
                PropertyType = typeof(TP),
                PropertyName = member.Name,
                Set = member.SetValue,
            };

            AddMap(mp);
            AddMap(mp, true);
        }

        public void Column<TP>(string column, Action<TSub, TP> lambda, bool prefixAlias = false)
        {
            var mp = new MapProperty
            {
                Column = new MapColumn(column, prefixAlias ? Alias : null),
                HostAlias = TableAlias,
                PropertyType = typeof(TP),
                PropertyName = column,
                Set = (x, y) => lambda((TSub)x, (TP)y),
            };

            AddMap(mp);
        }

        public void Self(Func<TSub, TSub, bool> match, Action<TSub, TSub> set)
        {
            Action<TSub, T> lambda = (a, b) =>
            {
                var items = Relation.GetChildren(b.GetType(), b).OfType<TSub>();
                var link = items.FirstOrDefault(x => match(a, x));
                if (!Equals(link, default(T)))
                    set(a, link);
            };
            if (SelfActions == null)
                SelfActions = new Dictionary<string, Action<object, object>>();
            SelfActions.Add(Alias, (x, y) => lambda((TSub)x, (T)y));
        }

        public void Link<TLink>(Action<TSub, TLink> lambda, string alias)
        {
            if (LinkActions == null)
                LinkActions = new Dictionary<string, Action<object, object>>();
            LinkActions.Add(alias, (x, y) => lambda((TSub)x, (TLink)y));
        }

        public virtual void Setup()
        {
            if (Keys.Count <= 0)
                Keys.AddMany(CreateIds());

            if (Columns.Count <= 0)
                Columns.AddMany(CreateProperties());
        }

        protected object SetValue(object parent, object sub)
        {
            if (IsMatch != null && !(bool)IsMatch.DynamicInvoke(sub)) return null;

            var result = Relation.Bind(parent, sub);

            if (result == sub || result == parent)
            {
                if (ActionFix != null)
                    ActionFix((TSub)sub);
            }

            return result;
        }

        public virtual object SetValue(object parent, Action<object, IEnumerable<IMapProperty>> read)
        {
            var sub = Activator.CreateInstance(ValueType);
            read(sub, Columns);
            var result = SetValue(parent, sub);
            if (sub != result && result != null)
                read(result, Columns);
            return result;
        }
    }
}